/* 2014/10/24 */
package com.cosmos.log4j;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Layout;
import org.apache.log4j.RollingFileAppender;
import org.apache.log4j.helpers.LogLog;

/**
 * ZipRollingFileAppender extends <code>RollingFileAppender</code> to backup the log files
 * and compresses every log file to zip file with new file name which contains
 * time stamp.
 * 
 * @author Leonardo
 * 
 */
public class ZipRollingFileAppender extends RollingFileAppender {

	/**
	 * Pattern of timestamp in log file. Reads log and finds start time and end
	 * time according to this pattern.
	 */
	protected static final Pattern TIMESTAMP_PATTERN = Pattern
			.compile("(\\d{4}\\-\\d{2}\\-\\d{2}\\s\\d{2}:\\d{2}:\\d{2}).+");

	/**
	 * Some characters are not valid in file name. These characters should be
	 * replaced.
	 */
	protected static final String TIMESTAMP_REPLACED_CHARACTER_REGEX = ":|\\s";

	/**
	 * This string is used to replace invalid file name characters in time
	 * stamp.
	 */
	protected static final String TIMESTAMP_REPLACE_CHARACTER = "-";

	/**
	 * Log file will be renamed to generate zip file. The file name inside zip
	 * file will be same.
	 */
	protected static final String LOG_RENAME_FORMAT = "%1$s(%3$s~%4$s)%2$s";

	protected String startTimeString = null;
	protected String endTimeString = null;

	/**
	 * Whether the log file be deleted after it has been compressed to zip file.
	 */
	protected boolean deleteAfterZip = false;

	public ZipRollingFileAppender() {
		super();
	}

	public ZipRollingFileAppender(Layout layout, String filename, boolean append)
			throws IOException {
		super(layout, filename, append);
	}

	public ZipRollingFileAppender(Layout layout, String filename)
			throws IOException {
		super(layout, filename);
	}

	@Override
	public void rollOver() {
		super.rollOver();

		/*
		 * Compress log file with new file name which contains time stamp.
		 */
		File logFile = new File(fileName + "." + 1);
		if (logFile.exists()) {
			try {
				readTimestamp(logFile);
			} catch (IOException | IllegalArgumentException ex) {
				LogLog.error("Read time stamp from log failed.", ex);
			}

			/*
			 * If time stamp cannot be read, do nothing.
			 */
			if (startTimeString != null && endTimeString != null) {
				startTimeString = startTimeString.replaceAll(
						TIMESTAMP_REPLACED_CHARACTER_REGEX,
						TIMESTAMP_REPLACE_CHARACTER);
				endTimeString = endTimeString.replaceAll(
						TIMESTAMP_REPLACED_CHARACTER_REGEX,
						TIMESTAMP_REPLACE_CHARACTER);

				/*
				 * Generates new log file name.
				 */

				// fileName filed may contains multiple levels of directory.
				String fileLastName = new File(fileName).getName();

				String newFileName = null;
				String zipFileName = null;
				int logExtensionIndex = fileLastName.lastIndexOf(".");
				if (logExtensionIndex == -1) {
					newFileName = String.format(LOG_RENAME_FORMAT,
							fileLastName, "", startTimeString, endTimeString);
					zipFileName = String.format(LOG_RENAME_FORMAT,
							fileLastName, ".zip", startTimeString,
							endTimeString);
				} else {
					String fileNameOnly = fileLastName.substring(0,
							logExtensionIndex);
					String fileExtension = fileLastName.substring(
							logExtensionIndex, fileLastName.length());

					newFileName = String.format(LOG_RENAME_FORMAT,
							fileNameOnly, fileExtension, startTimeString,
							endTimeString);
					zipFileName = String.format(LOG_RENAME_FORMAT,
							fileNameOnly, ".zip", startTimeString,
							endTimeString);
				}

				/*
				 * Compresses log file to zip.
				 */
				boolean zipSuccess = false;
				SingleFileZipper zipper = new SingleFileZipper();
				try {
					zipper.zip(logFile, newFileName, zipFileName);
					zipSuccess = true;
				} catch (IOException e) {
					LogLog.error("Compress log file failed.");
				}

				if (zipSuccess && deleteAfterZip) {
					logFile.delete();
				}
			}
		}
	}

	public boolean isDeleteAfterZip() {
		return deleteAfterZip;
	}

	public void setDeleteAfterZip(boolean deleteLogAfterZip) {
		this.deleteAfterZip = deleteLogAfterZip;
	}

	/**
	 * Reads log file, find start time stamp and end time stamp.
	 * 
	 * @param logFile
	 * @throws IOException
	 *             If reads file failed.
	 * @throws IllegalArgumentException
	 *             If time stamp cannot be found.
	 */
	protected void readTimestamp(File logFile) throws IOException,
			IllegalArgumentException {
		startTimeString = null;
		endTimeString = null;
		FileReader fileReader = null;
		BufferedReader bufferedReader = null;

		try {
			fileReader = new FileReader(logFile);
			bufferedReader = new BufferedReader(fileReader);
			String line;
			Matcher timeStampMatcher;

			// Read start time stamp.
			while ((line = bufferedReader.readLine()) != null) {
				timeStampMatcher = TIMESTAMP_PATTERN.matcher(line);
				if (timeStampMatcher.matches()) {
					startTimeString = timeStampMatcher.group(1);
					break;
				}
			}

			if (startTimeString == null) {
				throw new IllegalArgumentException(
						"No start time can be found in log file.");
			}

			// Read end time stamp.
			do {
				timeStampMatcher = TIMESTAMP_PATTERN.matcher(line);
				if (timeStampMatcher.matches()) {
					endTimeString = timeStampMatcher.group(1);
				}
			} while ((line = bufferedReader.readLine()) != null);

			if (endTimeString == null) {
				throw new IllegalArgumentException(
						"No end time can be found in log file.");
			}

		} catch (IOException ex) {
			throw new IOException("Read timestamp from log file failed.", ex);
		} finally {
			close(bufferedReader);
			close(fileReader);
		}
	}

	private void close(Closeable resource) {
		if (resource != null) {
			try {
				resource.close();
			} catch (IOException ex) {
				LogLog.error("Close resource failed.", ex);
			}
		}
	}
}
